/*
 * @(#)Entity.java        1.0 19-Apr-2009
 *
 * Copyright 2009 Thomas Ford. All rights reserved.
 */


package edu.napier.soc.xfdm.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The {@code Entity} class represents an entity in the functional data model.
 * It constructs itself based on the entity type which it is made from,
 * therefore all functions and constraints apply to an {@code Entity} object.
 *
 * @author Thomas Ford
 * @version 1.0, 22-Feb-2009
 */
public final class Entity implements Serializable {

	private static final long serialVersionUID = 34535352475674l;

	private final EntityType entityType;
	private final Map<Function, Object> binding;

	/**
	 * Constructs a new {@code Entity} object which is derived from
	 * {@code entityType}.
	 *
	 * @param entityType the entity type to be derived from.
	 */
	public Entity(EntityType entityType) {
		this(entityType, new HashMap<Function, Object>());
	}

	/**
	 * Constructs a new {@code Entity} object which is derived from
	 * {@code entityType} with given values. The values given in the {@code Map}
	 * should be a subset of the functions in the entity type, those which are
	 * not in the entity type will be ignored.
	 *
	 * @param entityType the entity type to be derived from.
	 * @param values     the values to be used.
	 */
	public Entity(EntityType entityType, Map<Function, Object> values) {
		if (entityType == null) {
			throw new NullPointerException("entityType must not be null");
		}
		if (values == null) {
			throw new NullPointerException("values must not be null");
		}
		this.entityType = entityType;
		binding = new HashMap<Function, Object>();

		List<Function> functions = entityType.getFunctions();

		for (Function a : functions) {
			Object value = values.get(a);
			binding.put(a, value);
		}
		checkTypes();
	}

	/**
	 * This is usually called from the entity type when a column is added /
	 * removed.
	 */
	public void justifyColumns() {
		List<Function> functions = entityType.getFunctions();

		for (Function a : functions) {
			if (!binding.containsKey(a)) {
				binding.put(a, null);
			}
		}

		for (Function a : binding.keySet()) {
			if (!functions.contains(a)) {
				binding.remove(a);
			}
		}
	}

	/**
	 * Removes a given value from a specified function if the function is multi
	 * valued (i.e. in a list).
	 *
	 * @param f     the function the value resides in.
	 * @param value the value to be removed.
	 * @return {@code true} if the value was removed, otherwise {@code false}.
	 */
	public boolean removeValue(Function f, Object value) {
		Object o = binding.get(f);

		if (o instanceof List) {
			List l = (List) o;
			return l.remove(value);
		} else if (o == value) {
			binding.put(f, null);
			return true;
		}
		return false;
	}

	/**
	 * @param name the name of the function to return.
	 * @return the requested function if it exists, otherwise {@code null}.
	 */
	public Function getFunction(String name) {
		return entityType.getFunction(name);
	}

	private void checkTypes() {
		for (Map.Entry<Function, Object> e : binding.entrySet()) {
			if (!e.getKey().validateValue(e.getValue())) {
				throw new IllegalArgumentException("Value " + e.getValue() +
						" is invalid type");
			}
		}
	}

	/**
	 * @return the entity type for this {@code Entity}.
	 */
	public EntityType getEntityType() {
		return entityType;
	}

	/**
	 * Returns the values of this {@code Entity} in the order in which they
	 * appear in the entity type.
	 *
	 * @return the values in this entity.
	 */
	public Object[] getValues() {
		List<Function> functions = entityType.getFunctions();
		Object[] values = new Object[functions.size()];

		for (Function a : functions) {
			values[functions.indexOf(a)] = binding.get(a);
		}
		return values;
	}

	/**
	 * @param f the function which contains the requested value.
	 * @return the value in the function (may be null).
	 */
	public Object getValue(Function f) {
		return binding.get(f);
	}

	/**
	 * @param name the name of the function which contains the requested value.
	 * @return the value in the function (may be null).
	 */
	public Object getValue(String name) {
		return getValue(getFunction(name));
	}

	/**
	 * Returns a copy of an {@code Entity}.
	 *
	 * @param e the entity to be copied.
	 * @return the copied entity.
	 */
	public static Entity copyOf(Entity e) {
		return new Entity(e.entityType, e.binding);
	}

	/**
	 * Sets a value of a given function. This method only checks a total and
	 * fixed constraints if the entity has already been added to the entity
	 * type, though it always checks fixed.
	 *
	 * @param f     the function of the value to be set.
	 * @param value the value to be set.
	 * @return {@code true} if the value was set, otherwise {@code false}.
	 * @throws edu.napier.soc.xfdm.model.ConstraintException
	 *          if the new value invalidates one or more constraints.
	 */
	public boolean setValue(Function f, Object value)
			throws ConstraintException {
		for (Constraint c : entityType.getConstraints()) {
			switch (c.getConstraintType()) {
				case FIXED:
					if (c.containsFunction(f)) {
						c.validateValue(this);
					}
					break;
				case TOTAL:
				case UNIQUE:
					if (entityType.containsEntity(this)
							&& c.containsFunction(f)) {
						Entity e = Entity.copyOf(this);
						e.setValue(f, value);
						c.validateValue(e);
					}
					break;
			}
		}
		List<Function> functions = entityType.getFunctions();

		if (!functions.contains(f)) {
			return false;
		}

		f.validateValue(value);
		binding.put(f, value);
		return true;
	}

	/**
	 * Adds a value to a specified function if it is multi-valued, otherwise it
	 * calls {@code setValue}. Constraints will be checked.
	 *
	 * @param f     the function of the value to be added.
	 * @param value the value to be added.
	 * @return {@code true} if the value was added, otherwise {@code false}.
	 * @throws edu.napier.soc.xfdm.model.ConstraintException
	 *          if one or more constraints are invalidated.
	 */
	public boolean addValue(Function f, Object value)
			throws ConstraintException {
		if (!f.isMultiValued()) {
			return setValue(f, value);
		}

		if (!entityType.containsFunction(f)) {
			return false;
		}

		f.validateValue(value);

		if (value == null) {
			return false;
		}
		Object o = binding.get(f);

		if (o instanceof List) {
			return ((List) o).add(value);
		} else {
			List<Object> l = new ArrayList<Object>();
			if (o != null) {
				l.add(o);
			}
			l.add(value);
			binding.put(f, l);
		}

		return true;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();

		for (Map.Entry<Function, Object> entry : binding.entrySet()) {
			if (entry.getKey().getReturnType() != ReturnType.ENTITY) {
				sb.append("Key: ");
				sb.append(entry.getKey());
				sb.append(", Value: ");
				sb.append(entry.getValue());
				sb.append('\n');
			}
		}
		return sb.toString();
	}
}